/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.loaders.internal;

import java.util.*;
import java.util.concurrent.*;
import ch.trackedbean.common.*;
import ch.trackedbean.loaders.*;

/**
 * Helper object forwarding change events to {@link IDataListener}s.
 * 
 * @author M. Hautle
 * @since 08.10.2010
 */
public class DataEventBroadcaster {
    /** The exception handler to use for exceptions raised by {@link IDataListener}s. */
    protected final IExceptionHandler exceptionHandler = DefaultExceptionHandler.getDefault();

    /** All listeners which were registered on a concrete type. */
    protected final ConcurrentHashMap<Class, CopyOnWriteArrayList<IDataListener>> concrete = new ConcurrentHashMap<Class, CopyOnWriteArrayList<IDataListener>>();

    /** All listeners which were registered on super types. */
    protected final CopyOnWriteArrayList<Entry> superType = new CopyOnWriteArrayList<Entry>();

    /**
     * Registers the given listener on the specified type.
     * 
     * @param l The listener
     * @param type The concrete type
     */
    public void addDataListener(IDataListener l, Class type) {
        CopyOnWriteArrayList<IDataListener> listeners = concrete.get(type);
        if (listeners == null) {
            concrete.putIfAbsent(type, new CopyOnWriteArrayList<IDataListener>());
            listeners = concrete.get(type);
        }
        listeners.addIfAbsent(l);
    }

    /**
     * Registers the given listener on the specified type and all it's subtypes.
     * 
     * @param l The listener
     * @param type The base type
     */
    public void addStructureDataListener(IDataListener l, Class type) {
        superType.addIfAbsent(new Entry(type, l));
    }

    /**
     * Removes the given listener from the specified type.
     * 
     * @param l The listener
     * @param type The concrete type
     */
    public void removeDataListener(IDataListener l, Class type) {
        final CopyOnWriteArrayList<IDataListener> listeners = concrete.get(type);
        if (listeners != null)
            listeners.remove(l);
    }

    /**
     * Removes the given listener from the specified type and all it's subtypes.
     * 
     * @param l The listener
     * @param type The base type
     */
    public void removeStructureDataListener(IDataListener l, Class type) {
        superType.remove(new Entry(type, l));
    }

    /**
     * Fires a changed event for the specified object type description.
     * 
     * @param <T> The object type
     * @param <D> The discriminator type
     * @param <K> The key type for the parameter map
     * @param <V> The value type for the parameter map
     * @param type The type of the concerned object
     * @param subType Additional discriminator specifying the "subtype" which is concerned (may be null)
     * @param settings Additional settings for the load (may be null)
     */
    public <T, D, K, V> void fireObjectsChangedEvent(Class<T> type, D subType, Map<K, V> settings) {
        // inform the concrete listeners first
        final CopyOnWriteArrayList<IDataListener> listeners = concrete.get(type);
        if (listeners != null) {
            for (IDataListener l : listeners)
                fireSecureComplex(l, type, subType, settings);
        }
        // and inform now all supertype listeners
        for (Entry e : superType) {
            if (e.isConcerned(type))
                fireSecureComplex(e.listener, type, subType, settings);
        }
    }

    /**
     * Fires a changed event for the specified object.
     * 
     * @param <T> The object type
     * @param <K> The key type
     * @param type The type of the object which has changed
     * @param key The key of the object which has changed
     */
    public <T, K> void fireObjectChangedEvent(Class<T> type, K key) {
        // inform the concrete listeners first
        final CopyOnWriteArrayList<IDataListener> listeners = concrete.get(type);
        if (listeners != null) {
            for (IDataListener l : listeners)
                fireSecureChange(l, type, key);
        }
        // and inform now all supertype listeners
        for (Entry e : superType) {
            if (e.isConcerned(type))
                fireSecureChange(e.listener, type, key);
        }
    }

    /**
     * Fires a created event for the specified object.
     * 
     * @param <T> The object type
     * @param <K> The key type
     * @param type The type of the object which was created
     * @param key The key of the object which was created
     */
    public <T, K> void fireObjectCreatedEvent(Class<T> type, K key) {
        // inform the concrete listeners first
        final CopyOnWriteArrayList<IDataListener> listeners = concrete.get(type);
        if (listeners != null) {
            for (IDataListener l : listeners)
                fireSecureCreate(l, type, key);
        }
        // and inform now all supertype listeners
        for (Entry e : superType) {
            if (e.isConcerned(type))
                fireSecureCreate(e.listener, type, key);
        }
    }

    /**
     * Fires a removed event for the specified object.
     * 
     * @param <T> The object type
     * @param <K> The key type
     * @param type The type of the object which was removed
     * @param key The key of the object which was removed
     */
    public <T, K> void fireObjectRemovedEvent(Class<T> type, K key) {
        // inform the concrete listeners first
        final CopyOnWriteArrayList<IDataListener> listeners = concrete.get(type);
        if (listeners != null) {
            for (IDataListener l : listeners)
                fireSecureRemove(l, type, key);
        }
        // and inform now all supertype listeners
        for (Entry e : superType) {
            if (e.isConcerned(type))
                fireSecureRemove(e.listener, type, key);
        }
    }

    /**
     * Calls {@link IDataListener#complexChange(Class, Object, Map)} forwarding any exception to the {@link #exceptionHandler}.
     * 
     * @param <T> The object type
     * @param <D> The discriminator type
     * @param <K> The key type for the parameter map
     * @param <V> The value type for the parameter map
     * @param l The listener
     * @param type The type of the concerned object
     * @param subType Additional discriminator specifying the "subtype" which is concerned (may be null)
     * @param settings Additional settings for the load (may be null)
     */
    private <D, T, K, V> void fireSecureComplex(IDataListener l, Class<T> type, D subType, Map<K, V> settings) {
        try {
            l.complexChange(type, subType, settings);
        } catch (Exception e) {
            exceptionHandler.exceptionOccurred(e);
        }
    }

    /**
     * Calls {@link IDataListener#changed(Class, Object)} forwarding any exception to the {@link #exceptionHandler}.
     * 
     * @param <T> The object type
     * @param <K> The key type
     * @param l The listener
     * @param type The type of the object which has changed
     * @param key The key of the object which has changed
     */
    private <T, K> void fireSecureChange(IDataListener l, Class<T> type, K key) {
        try {
            l.changed(type, key);
        } catch (Exception e) {
            exceptionHandler.exceptionOccurred(e);
        }
    }

    /**
     * Calls {@link IDataListener#created(Class, Object)} forwarding any exception to the {@link #exceptionHandler}.
     * 
     * @param <T> The object type
     * @param <K> The key type
     * @param l The listener
     * @param type The type of the object which has changed
     * @param key The key of the object which has changed
     */
    private <T, K> void fireSecureCreate(IDataListener l, Class<T> type, K key) {
        try {
            l.created(type, key);
        } catch (Exception e) {
            exceptionHandler.exceptionOccurred(e);
        }
    }

    /**
     * Calls {@link IDataListener#removed(Class, Object)} forwarding any exception to the {@link #exceptionHandler}.
     * 
     * @param <T> The object type
     * @param <K> The key type
     * @param l The listener
     * @param type The type of the object which has changed
     * @param key The key of the object which has changed
     */
    private <T, K> void fireSecureRemove(IDataListener l, Class<T> type, K key) {
        try {
            l.removed(type, key);
        } catch (Exception e) {
            exceptionHandler.exceptionOccurred(e);
        }
    }

    /**
     * Struct holding a {@link IDataListener} and the base type in which it is interested.
     * 
     * @author M. Hautle
     * @since 08.10.2010
     */
    private static class Entry {
        /** The base type. */
        private final Class type;

        /** The listener. */
        private final IDataListener listener;

        /**
         * Default constructor.
         * 
         * @param type The base type
         * @param listener The listener
         */
        private Entry(Class type, IDataListener listener) {
            this.type = type;
            this.listener = listener;
        }

        /**
         * Returns wherever this listener has to be informed about a change of the given class.
         * 
         * @param clazz The class of the event to fire
         * @return True if the listener has to be informed.
         */
        @SuppressWarnings("unchecked")
        boolean isConcerned(Class clazz) {
            return type.isAssignableFrom(clazz);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int hashCode() {
            int result = 31 + listener.hashCode();
            result = 31 * result + type.hashCode();
            return result;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            final Entry other = (Entry) obj;
            return type.equals(other.type) && listener.equals(other.listener);
        }
    }
}
